<?php


/**
 * @file
 * Field module functionality for the Organic groups module.
 */

/**
 * Implements hook_field_info().
 */
function og_field_info() {
  return array(
    'group' => array(
      'label' => t('Group audience'),
      'description' => t('This field stores groups associated with the content.'),
      'default_widget' => OG_AUDIENCE_WIDGET,
      'default_formatter' => 'og_list_default',
      // We do not expose the field as entity property, but we provide a
      // separate property making use of the membership entities instead.
    ),
  );
}

/**
 * Implements hook_field_formatter_info().
 */
function og_field_formatter_info() {
  return array(
    'og_list_default' => array(
      'label' => t('Group default list'),
      'field types' => array('group'),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function og_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();

  if ($field['field_name'] == OG_AUDIENCE_FIELD && !empty($items[0])) {
    foreach ($items as $delta => $item) {
      if ($group = og_get_group('group', $item['gid'])) {
        if ($group->access()) {
          $label = og_label($group->gid);
          $entity = entity_load($group->entity_type, array($group->etid));
          $entity = current($entity);
          // Get the entity type of the group entity.
          $uri = entity_uri($group->entity_type, $entity);
          $element[$delta] = array(
            '#type' => 'link',
            '#title' => $label,
            '#href' => $uri['path'],
          );
        }
        else {
          // No need to show private groups several times, so remember if it was
          // already added.
          $added = &drupal_static(__FUNCTION__, FALSE);

          if (!$added) {
            $added = TRUE;
            $element[$delta] = array(
              '#markup' => '- ' . t('Private group') . ' -',
            );
          }
        }
      }
    }
  }

  return $element;
}


/**
 * Implements hook_field_widget_info().
 */
function og_field_widget_info() {
  return array(
    OG_AUDIENCE_WIDGET => array(
      'label' => t('Group audience'),
      'field types' => array('group'),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
      ),
      'settings' => array(
        'opt_group' => 'auto',
      ),
    ),
    OG_AUDIENCE_AUTOCOMPLETE_WIDGET => array(
      'label'       => t('Autocomplete text field'),
      'description' => t('Display the list of referenceable groups as a textfield with autocomplete behaviour.'),
      'field types' => array('group'),
      'settings'    => array(
        'autocomplete_match' => 'contains',
        'size' => 60,
        'autocomplete_path' => 'group/autocomplete',
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_settings_form().
 */
function og_field_widget_settings_form($field, $instance) {
  $widget   = $instance['widget'];
  $defaults = field_info_widget_settings($widget['type']);
  $settings = array_merge($defaults, $widget['settings']);
  $form = array();

  if ($widget['type'] == OG_AUDIENCE_WIDGET) {

    $form['opt_group'] = array(
      '#type' => 'radios',
      '#title' => t('Input type'),
      '#description' => t('Select the input type that should be used to get the groups audience. Note that the <em>Always show "other groups"</em> option will show all groups including the ones the user is a not a member of.'),
      '#options' => array(
        'auto' => t('Automatically decide the input according to user permissions (Recommended)'),
        'never' => t('Never show "other groups"'),
        'always' => t('Always show "other groups"'),
      ),
      '#default_value' => $settings['opt_group'],
      '#required' => TRUE,
    );
  }
  elseif ($widget['type'] == OG_AUDIENCE_AUTOCOMPLETE_WIDGET) {
    $form['autocomplete_match'] = array(
      '#type'             => 'select',
      '#title'            => t('Autocomplete matching'),
      '#default_value'    => $settings['autocomplete_match'],
      '#options'          => array(
        'starts_with'     => t('Starts with'),
        'contains'        => t('Contains'),
      ),
      '#description'      => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of nodes.'),
    );
    $form['size'] = array(
      '#type'             => 'textfield',
      '#title'            => t('Size of textfield'),
      '#default_value'    => $settings['size'],
      '#element_validate' => array('element_validate_integer_positive'),
      '#required'         => TRUE,
    );
  }

  return $form;
}

/**
 * Implements hook_field_widget_form().
 *
 * Unlike options_field_widget_form() our widget's logic is a bit different, as
 * the form element type is a result of the user's access to the groups.
 * For example a privileged user may see all groups as an optgroup select list,
 * where the groups are divided to "My groups" and "Other groups". This means
 * that the $element['#type'] is a result of the options passed to
 * $element['#options'].
 */
function og_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $base) {
  $element = $base;
  $widget = $instance['widget'];

  switch ($widget['type']) {
    case OG_AUDIENCE_AUTOCOMPLETE_WIDGET:
      $element += array(
        '#type' => 'textfield',
        '#default_value' => isset($items[$delta]['gid']) ? $items[$delta]['gid'] : NULL,
        '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $field['field_name'],
        '#size' => $instance['widget']['settings']['size'],
        '#element_validate' => array('og_field_audience_autocomplete_validate'),
        '#value_callback' => 'og_field_audience_autocomplete_value',
      );

      $return = array('gid' => $element);

      break;

    case OG_AUDIENCE_WIDGET:
      // Determine if a user may see other groups as-well.
      $opt_group = FALSE;

      if ($instance['widget']['settings']['opt_group'] == 'always' || ($instance['widget']['settings']['opt_group'] == 'auto' && user_access('administer group'))) {
        $opt_group = TRUE;
      }

      // Get all the groups divided to "content groups" and "other groups".
      $audience = og_field_audience_options($opt_group);

      // Get all groups that should be excluded.
      $excludes = array();
      // If it's an existing group, then exclude itself, as in some cases a group
      // can act also as a group content, but we want to prevent associating the
      // group to itself.
      if (!empty($form['#' . $element['#entity_type']])) {
        list($id) = entity_extract_ids($element['#entity_type'], $form['#' . $element['#entity_type']]);
        if (($group = og_get_group($element['#entity_type'], $id))) {
          $excludes[$group->gid] = $group->gid;
        }
      }

      // Get default values from URL, or from the edited entity.
      $default_values = og_get_context_by_url();
      // Keep the group ID of the selected items, as they might be needed again,
      // and we don't want to iterate over the items again.
      $items_gid = array();
      if (!empty($items)) {
        foreach ($items as $item) {
          // Some groups might be already deleted, but we don't bulk
          // delete the fields of the entities associated with that group.
          // So make sure the hidden selected fields are only for existing
          // groups.
          if (in_array($item['gid'], $audience['content groups']) || in_array($item['gid'], $audience['other groups'])) {
            $default_values[$item['gid']] = $item['gid'];
            $items_gid[] = $item['gid'];
          }
        }
      }

      foreach (array('content groups', 'other groups') as $key) {
        if (!empty($audience[$key])) {
          // Get the label un-sanitized, as they will be laster sanitized
          // according to the form type.
          $audience[$key] = og_label_multiple($audience[$key], FALSE);
        }
      }

      // The group options presented to the user.
      $options = array();
      $hidden_selected_gids = array();
      $type = 'select';
      if ($opt_group) {
        // Show "My groups" and "Other groups".
        if ($my_groups = array_diff_key($audience['content groups'], $excludes)) {
          $options += array(t('My groups') => $my_groups);
        }
        if ($other_groups = array_diff_key($audience['other groups'], $excludes)) {
          $options += array(t('Other groups') => $other_groups);
        }
      }
      else {
        // Show only "My groups".
        $options = array_diff_key($audience['content groups'], $excludes);
        // If there are items that are already selected but the user doesn't
        // have access to them, we need to keep track of them.
        $hidden_selected_gids = drupal_map_assoc(array_diff($items_gid, array_keys($options)));
      }

      if (empty($options)) {
        // There are no group, so don't show any input element.
        $type = 'item';
        $element['#description'] = t('There are no groups you can select from.');
        $properties = array();
      }
      else {
        if (empty($element['#description'])) {
          $element['#description'] = t('Select the groups this content should be associated with.');
        }

        $element['#multiple'] = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;;
        // Don't make the field required, if there are no groups.
        $element['#required'] = $element['#required'] && !empty($options);

        $properties = _options_properties($type, $element['#multiple'], $element['#required'], $options);
        // If the element isn't required and cardinality is more than 1, and there
        // are some options, and a "none" option.
        if (!$element['#required'] && !$element['#multiple']) {
          // Use a dummy instance in order to use theme('options_none');
          $dummy_instance['widget']['type'] = 'options_select';
          $options = array('_none' => theme('options_none', array('instance' => $dummy_instance))) + $options;
        }
      }


      $element += array(
        // Input should be TRUE only if there are groups that can be selected.
        '#input' => $type != 'item',
        '#type' => $type,
        '#options' => $options,
        '#default_value' => $default_values,
        '#attributes' => array('class' => array('group-audience')),
        '#disabled' => empty($options),
        // Re-use options widget element validation, to correctly transform
        // submitted values from field => delta to delta => field.
        // @see options_field_widget().
        '#value_key' => 'gid',
        '#element_validate' => $type != 'item' ? array('options_field_widget_validate') : array(),
        '#properties' => $properties,
        // Add OG specific context.
        '#opt_group' => $opt_group,
        '#audience' => $audience,
        '#hidden_selected_gids' => $hidden_selected_gids,
      );

      $return = $element;
      break;
  }

  return $return;
}

/**
 * Implements hook_field_is_empty().
 */
function og_field_is_empty($item, $field) {
  return empty($item['gid']);
}

/**
 * Implements hook_field_attach_insert().
 */
function og_field_attach_insert($entity_type, $entity) {
  og_field_crud_group('insert', $entity_type, $entity);
}

/**
 * Implements hook_field_attach_update().
 */
function og_field_attach_update($entity_type, $entity) {
  og_field_crud_group('update', $entity_type, $entity);
}

/**
 * Implements hook_field_attach_delete().
 */
function og_field_attach_delete($entity_type, $entity) {
  og_field_crud_group('delete', $entity_type, $entity);
}

/**
 * Implements hook_field_insert().
 *
 * DEPRECATE WHEN FIELD SCHEMA CAN BE CHANGED.
 */
function og_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
  og_field_write('insert', $entity_type, $entity, $field, $instance, $langcode, $items);
}

/**
 * Implements hook_field_update().
 *
 * DEPRECATE WHEN FIELD SCHEMA CAN BE CHANGED.
 */
function og_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
  og_field_write('update', $entity_type, $entity, $field, $instance, $langcode, $items);
}

 /**
 * Insert or update a field record.
 *
 * DEPRECATE WHEN FIELD SCHEMA CAN BE CHANGED.
 *
 * Since we can not change the field schema we must still populate the fields
 * with non-null values.
 *
 * @param $op
 *   The operation - "insert" or "update".
 */
function og_field_write($op, $entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as &$item) {
    // Set default values.
    $item += array('state' => OG_STATE_ACTIVE, 'created' => time());
  }
}

/**
 * Implements hook_field_attach_form().
 *
 * Handle translated nodes that act as groups.
 */
function og_field_attach_form($entity_type, $entity, &$form, $form_state, $langcode) {
  if (!empty($form['#node_edit_form']) &&
    (og_is_group_type('node', $form['type']['#value']) || og_is_group_content_type('node', $form['type']['#value'])) &&
    (!empty($entity->tnid) && $entity->tnid != $entity->nid) || (!empty($_GET['translation']) && !empty($_GET['target']))) {

    // Iterate over OG fields that shouldn't be changed in node translation.
    foreach (og_fields_info() as $field_name => $info) {
      if (!empty($form[$field_name]) && $info['disable on node translate']) {
        // Prevent changing the group state on nodes that are translated.
        $description = t('You can not change "@label" field from a translated content.', array('@label' => $info['instance']['label']));

        $tnid = !empty($entity->tnid) ? $entity->tnid : $_GET['translation'];
        if (($node = node_load($tnid)) && node_access('update', $node)) {
          $description .= ' ' . t('Changing this field can only be done via <a href="@node">@title</a>.', array('@node' => url('node/' . $node->nid . '/edit'), '@title' => $node->title));
        }

        $form[$field_name][LANGUAGE_NONE]['#options'] = array();
        $form[$field_name][LANGUAGE_NONE]['#description'] = $description;
        $form[$field_name][LANGUAGE_NONE]['#disabled'] = TRUE;
        $form[$field_name][LANGUAGE_NONE]['#required'] = FALSE;

      }
    }
  }
}

/**
 * Validate handler; Re-add hidden selected gids if exist.
 */
function og_field_widget_form_validate($form, &$form_state) {
  if (!empty($form[OG_AUDIENCE_FIELD][LANGUAGE_NONE]['#hidden_selected_gids']) && $gids = $form[OG_AUDIENCE_FIELD][LANGUAGE_NONE]['#hidden_selected_gids']) {
    // Rebuild the form state values.
    foreach ($form_state['values'][OG_AUDIENCE_FIELD][LANGUAGE_NONE] as $key => $value) {
      $gids[$value['gid']] = $value['gid'];
    }

    $delta = 0;
    $value = array();
    foreach ($gids as $gid) {
      $value[LANGUAGE_NONE][$delta]['gid'] = $gid;
      ++$delta;
    }
    // Set the new values.
    form_set_value($form[OG_AUDIENCE_FIELD], $value, $form_state);
  }
}

/**
 * Create update or delete a group, based on the field CRUD.
 *
 * TODO: Remove this an move to hook_entity_presave().
 *
 * @see og_field_attach_insert().
 * @see og_field_attach_update().
 * @see og_field_attach_delete().
 */
function og_field_crud_group($op, $entity_type, $entity) {
  $property = OG_GROUP_FIELD;

  // If the entity is a translated node, we can return early, as all operations
  // happen on the source node.
  if ($entity_type == 'node'
    && ((!empty($entity->tnid) && $entity->tnid != $entity->nid) || !empty($entity->translation_source->tnid))
    && module_exists('translation')) {
    return;
  }

  if (!empty($entity->{$property}) && empty($entity->og_skip_group_create)) {
    $wrapper = &$entity->{$property}[LANGUAGE_NONE];

    // Get the entity ID.
    list($id) = entity_extract_ids($entity_type, $entity);

    $group = og_get_group($entity_type, $id, TRUE, array(OG_STATE_ACTIVE, OG_STATE_PENDING));
    if ($op == 'delete') {
      if (!empty($group->gid)) {
        // Remove group.
        $group->delete();
      }
    }
    else {
      // Check group is new.
      if (empty($group->gid)) {
        if (!empty($wrapper[0]['value'])) {
          // Save the group to get the group ID.
          $group->save();

          // Subscribe the entity author, if exists.
          if (!empty($entity->uid) && ($account = user_load($entity->uid))) {
            $values = array('entity' => $account);
            og_group($group->gid, $values);
          }
        }
      }
      else {
        // Existing group.
        $save = FALSE;

        if ($group->state == OG_STATE_ACTIVE && empty($wrapper[0]['value'])) {
          $group->state = OG_STATE_PENDING;
          $save = TRUE;
        }
        elseif($group->state == OG_STATE_PENDING && !empty($wrapper[0]['value'])) {
          $group->state = OG_STATE_ACTIVE;
          $save = TRUE;
        }

        // Check if the entity label has changed.
        $label = og_entity_label($entity_type, $entity);

        if ($group->label != $label) {
          $group->label = $label;
          $save = TRUE;
        }

        if ($save) {
          $group->save();
        }
      }

      // Determine if field has changed and roles should be overridden, or
      // reverted, by comparing the default access field of the entity being
      // saved, and its original state.
      $property = OG_DEFAULT_ACCESS_FIELD;
      // The field exists.
      if (isset($entity->{$property})) {
        if (!empty($entity->{$property}[LANGUAGE_NONE][0]['value'])) {
          og_roles_override($group->gid);
        }
        elseif (empty($group->is_new)) {
          // If the field is set to be using default access and there are
          // already overridden roles we delete them.
          og_delete_user_roles_by_group($group->gid);
        }
      }
    }
  }
}


/**
 * Get an array of allowed values for OG audience field.
 *
 * @return
 *   Array keyed by "content groups" and "other groups".
 */
function og_field_audience_options(&$opt_group = FALSE, $account = NULL) {
  $return = &drupal_static(__FUNCTION__, array());

  if (!empty($return)) {
    return $return;
  }

  if (empty($account)) {
    global $user;

    $account = clone($user);
  }

  //Initialize values.
  $return = array('content groups' => array(), 'other groups' => array());

  $content_groups = og_get_entity_groups('user', $account);
  $return['content groups'] = $content_groups;

  // Get all other groups.
  $all_groups = og_get_all_group();
  $return['other groups'] = array_diff($all_groups, $content_groups);

  // Allow other modules to change the audience options. While it can be done
  // via hook_form_alter(), it will require other modules to know too much of
  // the internal work.
  drupal_alter('og_audience_options', $return, $opt_group, $account);

  return $return;
}

/**
 * Implements hook_field_validate().
 *
 * FIXME: Adapt this function to OG.
 *
 * Possible error codes:
 * - 'invalid_nid': nid is not valid for the field (not a valid node id, or the node is not referenceable).
 */
function __og_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  // Extract nids to check.
  $ids = array();

  // First check non-numeric "nid's to avoid losing time with them.
  foreach ($items as $delta => $item) {
    if (is_array($item) && !empty($item['nid'])) {
      if (is_numeric($item['nid'])) {
        $ids[] = $item['nid'];
      }
      else {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => 'invalid_nid',
          'message' => t("%name: invalid input.",
             array('%name' => $instance['label'])),
        );
      }
    }
  }
  // Prevent performance hog if there are no ids to check.
  if ($ids) {
    $refs = og_field_audience_potential_groups('', NULL, $ids);
    foreach ($items as $delta => $item) {
      if (is_array($item)) {
        if (!empty($item['nid']) && !isset($refs[$item['nid']])) {
          $errors[$field['field_name']][$langcode][$delta][] = array(
            'error' => 'invalid_nid',
            'message' => t("%name: this post can't be referenced.",
              array('%name' => $instance['label'])),
          );
        }
      }
    }
  }
}


/**
 * Value callback for a node_reference autocomplete element.
 *
 * Replace the node nid with a node title.
 */
function og_field_audience_autocomplete_value($element, $input = FALSE, $form_state) {
  if ($input === FALSE) {
    // We're building the displayed 'default value': expand the raw nid into
    // "Group title [gid:n]".
    if (!empty($element['#default_value']) && $group = og_load($element['#default_value'])) {
      $value = og_label($group->gid);
      $value .= ' [gid:' . $group->gid . ']';
      return $value;
    }
  }
}

/**
 * Validation callback for a group audience autocomplete element.
 */
function og_field_audience_autocomplete_validate($element, &$form_state, $form) {
  $field = $form_state['field'][$element['#field_name']][$element['#language']]['field'];
  $instance = $form_state['field'][$element['#field_name']][$element['#language']]['instance'];

  $value = $element['#value'];
  $gid = NULL;

  if (!empty($value)) {
    // Check whether we have an explicit "[gid:n]" input.
    preg_match('/^(?:\s*|(.*) )?\[\s*gid\s*:\s*(\d+)\s*\]$/', $value, $matches);
    if (!empty($matches)) {
      // Explicit gid. Check that the 'title' part matches the actual title for
      // the nid.
      list(, $label, $gid) = $matches;
      if (!empty($label)) {
        if ($label != og_label($gid)) {
          form_error($element, t('%name: label mismatch. Please check your selection.', array('%name' => $instance['label'])));
        }
      }
    }
    else {
      // No explicit gid (the submitted value was not populated by autocomplete
      // selection). Get the gid of a referencable node from the entered title.
      if ($reference = og_field_audience_potential_groups($value, 'equals', NULL, 1)) {
        $gid = key($reference);
      }
      else {
        form_error($element, t('%name: found no valid group with that label.', array('%name' => $instance['label'])));
      }
    }
  }

  // Set the element's value as the node id that was extracted from the entered
  // input.
  form_set_value($element, $gid, $form_state);
}

/**
 * Implements hook_field_widget_error().
 */
function og_field_widget_error($element, $error, $form, &$form_state) {
  form_error($element, $error['message']);
}

/**
 * Fetch an array of all candidate groups.
 *
 * This info is used in various places (allowed values, autocomplete
 * results, input validation...). Some of them only need the nids,
 * others nid + titles, others yet nid + titles + rendered row (for
 * display in widgets).
 *
 * The array we return contains all the potentially needed information,
 * and lets consumers use the parts they actually need.
 *
 * @param $field
 *   The field description.
 * @param $string
 *   Optional string to filter titles on (used by autocomplete).
 * @param $match
 *   Operator to match filtered name against, can be any of:
 *   'contains', 'equals', 'starts_with'
 * @param $ids
 *   Optional node ids to lookup (the $string and $match arguments will be
 *   ignored).
 * @param $limit
 *   If non-zero, limit the size of the result set.
 *
 * @return
 *   An array of valid nodes in the form:
 *   array(
 *     gid => 'rendered' -- The text to display in widgets (can be HTML)
 *   );
 *  @todo Check whether we still need the 'rendered' value (hook_options_list()
 *  does not need it anymore). Should probably be clearer after the 'Views'
 *  mode is ported.
 */
function og_field_audience_potential_groups($string = '', $match = 'contains', $ids = array(), $limit = NULL) {
  $results = &drupal_static(__FUNCTION__, array());

  // Create unique id for static cache.
  $cid = $match . ':'
    . ($string !== '' ? $string : implode('-', $ids))
    . ':' . $limit;
  if (!isset($results[$cid])) {
    $groups = og_field_audience_potential_groups_standard($string, $match, $ids, $limit);

    // Store the results.
    $results[$cid] = !empty($groups) ? $groups : array();
  }

  return $results[$cid];
}

/**
 * Helper function for og_field_audience_potential_groups().
 */
function og_field_audience_potential_groups_standard($string = '', $match = 'contains', $ids = array(), $limit = NULL) {
  $query = og_get_all_group(array(OG_STATE_ACTIVE), array('return query' => TRUE));

  $query->addField('og', 'label');
  if ($string !== '') {
    $args = array();
    switch ($match) {
      case 'contains':
        $title_clause = 'label LIKE :match';
        $args['match'] = '%' . $string . '%';
        break;

      case 'starts_with':
        $title_clause = 'label LIKE :match';
        $args['match'] = $string . '%';
        break;

      case 'equals':
      default: // no match type or incorrect match type: use "="
        $title_clause = 'label = :match';
        $args['match'] = $string;
        break;
    }
    $query->where($title_clause, $args);
  }
  elseif ($ids) {
    $query->condition('gid', $ids, 'IN');
  }

  $query->orderBy('label');

  if ($limit) {
    $query->range(0, $limit);
  }

  $gids = $query->execute()->fetchAllKeyed();
  $groups = og_load_multiple(array_keys($gids));
  foreach ($groups as $group) {
    $label = og_label($group->gid);
    $return[$group->gid] = $label;
  }
  return $return;
}

/**
 * Menu callback for the autocomplete results.
 */
function og_field_audience_autocomplete($field_name, $string = '') {
  $field = field_info_field($field_name);

  $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains';
  $matches = array();

  $groups = og_field_audience_potential_groups($string, $match, array(), 10);
  foreach ($groups as $gid => $label) {
    // Add a class wrapper for a few required CSS overrides.
    $matches[$label . " [gid:$gid]"] = '<div class="group-autocomplete">' . $label . '</div>';
  }
  drupal_json_output($matches);
}